﻿using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace CodeAnalysisRuleCollection.Maths.CSharp
{
    /// <example>boolean == true</example>
    [DiagnosticAnalyzer]
    [ExportDiagnosticAnalyzer(DiagnosticId, LanguageNames.CSharp)]
    internal class UnnecessaryComparisonToTrueDiagnosticAnalyzer : ISyntaxNodeAnalyzer<SyntaxKind>
    {
        public const string DiagnosticId = "UnnecessaryComparisonToTrue";

        public static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
            DiagnosticId,
            "Unnecessary comparison to true",
            "It is not necessary to compare a boolean to another boolean value",
            "Performance",
            DiagnosticSeverity.Warning
        );

        public ImmutableArray<SyntaxKind> SyntaxKindsOfInterest
        {
            get
            {
                return ImmutableArray.Create(
                    SyntaxKind.EqualsExpression,
                    SyntaxKind.NotEqualsExpression
                );
            }
        }

        public ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }

        public void AnalyzeNode(SyntaxNode node, SemanticModel semanticModel, Action<Diagnostic> addDiagnostic, CancellationToken cancellationToken)
        {
            if ((var binary = node as BinaryExpressionSyntax) != null)
            {
                Optional<object>
                    leftValue = semanticModel.GetConstantValue(binary.Left, cancellationToken),
                    rightValue = semanticModel.GetConstantValue(binary.Right, cancellationToken)
                ;
                
                bool
                    isLeftFalse = leftValue.HasValue && leftValue.Value.Equals(false),
                    isRightFalse = rightValue.HasValue && rightValue.Value.Equals(false),
                    isLeftTrue = leftValue.HasValue && leftValue.Value.Equals(true),
                    isRightTrue = rightValue.HasValue && rightValue.Value.Equals(true)
                ;

                // Assume, that the type of the expression on the other side is boolean also,
                // so the binary operation we are diagnosing is not an overload like
                // public static TResult operator ==(T left, bool right)

                if (
                    (binary.IsKind(SyntaxKind.EqualsExpression) && (isLeftTrue || isRightTrue)) ||
                    (binary.IsKind(SyntaxKind.NotEqualsExpression) && (isLeftFalse || isRightFalse))
                )
                    addDiagnostic(Diagnostic.Create(Rule, node.GetLocation()));
            }
        }
    }

    [ExportCodeFixProvider(UnnecessaryComparisonToTrueDiagnosticAnalyzer.DiagnosticId, LanguageNames.CSharp)]
    internal class UnnecessaryBinaryOperationCodeFixProvider : ICodeFixProvider
    {
        public IEnumerable<string> GetFixableDiagnosticIds()
        {
            return new[] { UnnecessaryComparisonToTrueDiagnosticAnalyzer.DiagnosticId };
        }

        public async Task<IEnumerable<CodeAction>> GetFixesAsync(Document document, TextSpan span, IEnumerable<Diagnostic> diagnostics, CancellationToken cancellationToken)
        {
            var root = await document.GetSyntaxRootAsync(cancellationToken);

            var diagnosticSpan = diagnostics.First().Location.SourceSpan;

            // find the binary expression identified by the diagnostic
            var binary = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<BinaryExpressionSyntax>().First();

            // return a code action that will invoke the fix
            return new[] { CodeAction.Create("Remove unnecessary comparison", c => Fix(root, document, binary, c)) };
        }

        private async Task<Document> Fix(SyntaxNode root, Document document, BinaryExpressionSyntax binary, CancellationToken cancellationToken)
        {
            SemanticModel model = await document.GetSemanticModelAsync(cancellationToken);

            Optional<object>
                leftValue = model.GetConstantValue(binary.Left, cancellationToken),
                rightValue = model.GetConstantValue(binary.Right, cancellationToken)
            ;

            bool
                isLeftFalse = leftValue.HasValue && leftValue.Value.Equals(false),
                isRightFalse = rightValue.HasValue && rightValue.Value.Equals(false),
                isLeftTrue = leftValue.HasValue && leftValue.Value.Equals(true),
                isRightTrue = rightValue.HasValue && rightValue.Value.Equals(true)
            ;

            // get simplified value
            SyntaxNode replacement = (
                binary.IsKind(SyntaxKind.EqualsExpression) && isRightTrue ||
                binary.IsKind(SyntaxKind.NotEqualsExpression) && isRightFalse
                    ? binary.Left
                    : binary.Right
            )
                .WithAdditionalAnnotations(Formatter.Annotation)
            ;

            SyntaxNode newRoot = root.ReplaceNode(binary, replacement);
            return document.WithSyntaxRoot(newRoot);
        }
    }
}
